﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using PdfGeneratorNetFree.PgnEnum;
using PdfGeneratorNetFree.PgnFont;
using PdfGeneratorNetFree.PgnStyle;

namespace PdfGeneratorNetFree.PdfXmlParser
{
	using System.Reflection;
	using System.Xml;

	using PdfGeneratorNetFree;
	using PdfGeneratorNetFree.PdfContentItem;

	public class PdfXmlParser
	{
		public readonly PdfDocument PdfDoc = new PdfDocument();	// { get; private set; }
		public readonly Dictionary<string, AbstractStyle> StyleDic = new Dictionary<string, AbstractStyle>();
		public readonly Dictionary<string, PdfContent> ContentDic = new Dictionary<string, PdfContent>();

		private readonly Dictionary<string, string[]> pageContentsDic = new Dictionary<string, string[]>();

		public PdfXmlParser(string defineXmlPath)
		{
			var document = new XmlDocument();
			document.Load(defineXmlPath);
			if (document == null || document.DocumentElement == null)
			{
				throw new ArgumentException("invalid define xml.");
			}


			foreach (XmlNode node in document.DocumentElement)
			{
				var nodeName = node.Name;
				var elements = PdfXmlParser.GetXmlElements(node);
				switch (nodeName)
				{
					case "pdfFont":
						this.AddFont(elements);
						break;

					case "pdfPage":
						this.AddPage(elements);
						break;

					case "Style":
						this.SetStyle(elements);
						break;

					case "pdfContent":
						this.SetPdfContent(elements);
						break;

					case "documentSettings":
						this.SetDocumentSettings(elements);
						break;
				}
			}

			//ページとコンテントを結びつける。
			foreach (var page in this.pageContentsDic.Keys)
			{
				foreach (var content in this.pageContentsDic[page])
				{
					if (this.PdfDoc.HasContent(content))
					{
						this.PdfDoc.AddContent(page, content);
					}
					else
					{
						var con = this.ContentDic[content];
						this.PdfDoc.AddContent(page, con.Items, con.OffsetX, con.OffsetY, content);
					}
				}
			}
		} //Method

		private void AddFont(XmlElement[] elements)
		{
			foreach (var fontElement in elements)
			{
				var name = fontElement.GetAttribute("name");
				var fontPath = fontElement.GetAttribute("fontPath");

				switch (fontElement.Name)
				{
					case "font":
						var fontIndex = 0;
						int.TryParse(fontElement.GetAttribute("fontIndex"), out fontIndex);
						this.PdfDoc.AddFont(name, new PdfFont(fontPath, fontIndex));
						break;

					case "eudcFont":
						this.PdfDoc.AddFont(name, new PdfFontEudc(fontPath));
						break;
				}
			}
		}

		private void AddPage(XmlElement[] elements)
		{
			foreach (var element in elements)
			{
				if (element.Name != "page")
				{
					continue;
				}

				var name = element.GetAttribute("name");
				var paperSizeMethod = element.GetAttribute("paperSize");
				if (string.IsNullOrEmpty(paperSizeMethod))
				{
					this.PdfDoc.AddPage(name);
				}
				else
				{
					var paperSize = PdfXmlParser.GetPaperSize(paperSizeMethod, element.GetAttribute("paperSizeX"), element.GetAttribute("paperSizeY"));
					this.PdfDoc.AddPage(name, paperSize);
				}

				var contentStr = element.GetAttribute("contents");
				if (!string.IsNullOrEmpty(contentStr))
				{
					var contents = contentStr.Split(',');
					this.pageContentsDic.Add(name, contents);
				}
			}
		}


		private void SetStyle(XmlElement[] elements)
		{
			foreach (var element in elements)
			{
				AbstractStyle style = null;

				var name = element.GetAttribute("name");

				var color = Color.Empty;
				var colorStr = element.GetAttribute("color");
				if (!string.IsNullOrEmpty(colorStr))
				{
					color = (Color)PdfXmlParser.GetPropertyInvoke(new Color(), colorStr);
				}


				switch (element.Name)
				{
					case "textStyle":
						var fontName = element.GetAttribute("fontName");
						var eudcFontName = element.GetAttribute("eudcFontName");
						var fontSizeStr = element.GetAttribute("fontSize");
						var fontWeightStr = element.GetAttribute("fontWeight");
						var hAlignStr = element.GetAttribute("hAlign");
						var vAlignStr = element.GetAttribute("vAlign");
						var strokeStyleStr = element.GetAttribute("underLineStyle");
						style = this.CreateTextStyle(color, fontName, eudcFontName, fontSizeStr, fontWeightStr, hAlignStr, vAlignStr, strokeStyleStr);
						break;

					case "fillStyle":
						style = new FillStyle(color);
						break;

					case "strokeStyle":
						var width = float.Parse(element.GetAttribute("width"));


						var dashTypeStr = element.GetAttribute("dashType");
						DashType dash = null;
						if (!string.IsNullOrEmpty(dashTypeStr))
						{
							if (dashTypeStr == "Custom")
							{
								var strArray = element.GetAttribute("dashArray").Split(',');
								var dashArray = new float[strArray.Length];
								for (var i = 0; i < strArray.Length; i++)
								{
									dashArray[i] = float.Parse(strArray[i]);
								}

								dash = (DashType)PdfXmlParser.MethodInvoke<DashType>(null, dashTypeStr, dashArray);
							}
							else
							{
								dash = (DashType)PdfXmlParser.GetPropertyInvoke<DashType>(null, dashTypeStr);
							}
						}

						style = new StrokeStyle(width, color, dash);
						break;
				}

				if (style != null)
				{
					this.StyleDic.Add(name, style);
				}
			}
		}


		private TextStyle CreateTextStyle(Color color,  string fontName, string eudcFontName, string fontSizeStr, string fontWeightStr, string hAlignStr, string vAlignStr, string strokeStyleStr)
		{
			
			float fontSize;
			float.TryParse(fontSizeStr, out fontSize);

			FontWeight fontWeight = null;
			if (!string.IsNullOrEmpty(fontWeightStr))
			{
				fontWeight = (FontWeight)PdfXmlParser.GetPropertyInvoke<FontWeight>(null, fontWeightStr);
			}

			var hAlign = HorizontalAlign.NoneSpecified;
			if (!string.IsNullOrEmpty(hAlignStr))
			{
				hAlign = (HorizontalAlign)Enum.Parse(typeof(HorizontalAlign), hAlignStr);
			}

			var vAlign = VerticalAlign.NoneSpecified;
			if (!string.IsNullOrEmpty(vAlignStr))
			{
				vAlign = (VerticalAlign)Enum.Parse(typeof(VerticalAlign), vAlignStr);
			}

			StrokeStyle strokeStyle = null;
			if (!string.IsNullOrEmpty(strokeStyleStr))
			{
				if (!this.StyleDic.ContainsKey(strokeStyleStr))
				{
					throw new InvalidOperationException("StrokeStyle=" + strokeStyleStr + "が見つかりません。");
				}

				strokeStyle = (StrokeStyle)this.StyleDic[strokeStyleStr];
			}

			return new TextStyle(fontName, fontSize, color, fontWeight, hAlign, vAlign, strokeStyle, eudcFontName);
		}


		private void SetPdfContent(XmlElement[] elements)
		{
			foreach (var element in elements)
			{
				if (element.Name == "content")
				{
					var contentItemNodes = PdfXmlParser.GetXmlElements(element);
					var contentItems = this.GetPdfContentItems(contentItemNodes);

					var offsetX = element.GetAttribute("offsetX");
					var offsetY = element.GetAttribute("offsetY");

					var name = element.GetAttribute("name");
					var content = new PdfContent(name, contentItems, offsetX, offsetY);
					this.ContentDic.Add(name, content);
				}
			}
		}

		private Dictionary<string, AbstractPdfContentItem> GetPdfContentItems(XmlElement[] elements)
		{
			var result = new Dictionary<string, AbstractPdfContentItem>();
			foreach (var element in elements)
			{
				var name = element.GetAttribute("name");
				float x1, y1, x2, y2, x, y, width, height, rx, ry, rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4;
				float.TryParse(element.GetAttribute("x1"), out x1);
				float.TryParse(element.GetAttribute("y1"), out y1);
				float.TryParse(element.GetAttribute("x2"), out x2);
				float.TryParse(element.GetAttribute("y2"), out y2);

				float.TryParse(element.GetAttribute("x"), out x);
				float.TryParse(element.GetAttribute("y"), out y);
				float.TryParse(element.GetAttribute("width"), out width);
				float.TryParse(element.GetAttribute("height"), out height);

				float.TryParse(element.GetAttribute("rx"), out rx);
				float.TryParse(element.GetAttribute("ry"), out ry);
				float.TryParse(element.GetAttribute("rx1"), out rx1);
				float.TryParse(element.GetAttribute("ry1"), out ry1);
				float.TryParse(element.GetAttribute("rx2"), out rx2);
				float.TryParse(element.GetAttribute("ry2"), out ry2);
				float.TryParse(element.GetAttribute("rx3"), out rx3);
				float.TryParse(element.GetAttribute("ry3"), out ry3);
				float.TryParse(element.GetAttribute("rx4"), out rx4);
				float.TryParse(element.GetAttribute("ry4"), out ry4);

				var str = element.GetAttribute("string");

				var styleStr1 = element.GetAttribute("strokeStyle");
				if (string.IsNullOrEmpty(styleStr1))
				{
					styleStr1 = element.GetAttribute("textStyle");
				}

				AbstractStyle itemStyle = null;
				if (!string.IsNullOrEmpty(styleStr1))
				{
					itemStyle = this.StyleDic[styleStr1];
				}

				var styleStr2 = element.GetAttribute("fillStyle");
				AbstractStyle fillStyle = null;
				if (!string.IsNullOrEmpty(styleStr2))
				{
					fillStyle = this.StyleDic[styleStr2];
				}

				switch (element.Name)
				{
					case "line":
						result.Add(name, new Line(x1, y1, x2, y2, (StrokeStyle)itemStyle));
						break;

					case "text":
						result.Add(name, new Text(x, y, width, height, str, (TextStyle)itemStyle));
						break;

					case "rect":
						if (0 < rx1 + ry1 + rx2 + ry2 + rx3 + ry3 + rx4 + ry4)
						{
							result.Add(name, new Rect(x, y, width, height, (StrokeStyle)itemStyle, (FillStyle)fillStyle, rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4));
						}
						else
						{
							result.Add(name, new Rect(x, y, width, height, (StrokeStyle)itemStyle, (FillStyle)fillStyle, rx, ry));
						}

						break;
				}
			}

			return result;
		}

		private void SetDocumentSettings(XmlElement[] elements)
		{

			foreach (var element in elements)
			{
				switch (element.Name)
				{
					case "defaultPaper":
						var paperSizeMethod = element.GetAttribute("paperSize");
						if (!string.IsNullOrEmpty(paperSizeMethod))
						{
							var paperSize = PdfXmlParser.GetPaperSize(paperSizeMethod, element.GetAttribute("paperSizeX"), element.GetAttribute("paperSizeY"));
							this.PdfDoc.SetDefaultPaperSize(paperSize);
						}

						break;

					case "docInfo":
						var title = element.GetAttribute("title");
						var subTitle = element.GetAttribute("subTitle");
						var author = element.GetAttribute("author");
						this.PdfDoc.SetDocInfo(title, subTitle, author);
						break;

					case "encryptionInfo":
						var userPassword = element.GetAttribute("userPassword");
						var ownerPassword = element.GetAttribute("ownerPassword");
						var permissionStr = element.GetAttribute("permissions");
							this.PdfDoc.SetOpenPassword(userPassword);
						if (string.IsNullOrEmpty(permissionStr))
						{
							this.PdfDoc.SetPermissionControlPassword(ownerPassword);
						}
						else
						{
							var permission = (UserPermissions)Enum.Parse(typeof(UserPermissions), permissionStr);
							this.PdfDoc.SetPermissionControlPassword(ownerPassword, permission);
						}

						break;
				}
			}
		}

		private static XmlElement[] GetXmlElements(XmlNode xmlNode)
		{
			if (!xmlNode.HasChildNodes)
			{
				return new XmlElement[0];
			}

			var xmlElementList = new List<XmlElement>();
			foreach (XmlNode node in xmlNode.ChildNodes)
			{
				var xmlElement = node as XmlElement;
				if (xmlElement == null)
				{
					continue;
				}

				xmlElementList.Add(xmlElement);
			}

			return xmlElementList.ToArray();
		}

		/// <summary>
		/// PaperSize structを返す。
		/// </summary>
		/// <param name="paperSizeMethod">Method　or Property</param>
		/// <param name="customX">Custom以外は使用しない</param>
		/// <param name="customY">Custom以外は使用しない</param>
		/// <returns>PaperSize struct</returns>
		private static PaperSize GetPaperSize(string paperSizeMethod, string customX, string customY)
		{
			PaperSize result;
			if (paperSizeMethod == "Custom")
			{
				var paperSizeX = float.Parse(customX);
				var paperSizeY = float.Parse(customY);
				result = (PaperSize)PdfXmlParser.MethodInvoke(new PaperSize(), paperSizeMethod, paperSizeX, paperSizeY);
			}
			else
			{
				result = (PaperSize)PdfXmlParser.GetPropertyInvoke(new PaperSize(), paperSizeMethod);
			}

			return result;
		}


		/// <summary>
		/// 文字列として渡されたメソッドを実行する。
		/// </summary>
		/// <typeparam name="T">実行するメソッドを持つインスタンスの型</typeparam>
		/// <param name="instance">実行するメソッドのインスタンス。静的（static）なメソッドの場合はnull（構造体の場合はnew 構造体名）を指定し、MethodInvoke＜型＞()とする。</param>
		/// <param name="methodName">メソッド名</param>
		/// <param name="param">メソッドの引数。</param>
		/// <returns>メソッドの戻り値。object型であるためキャストすること</returns>
		internal static object MethodInvoke<T>(T instance, string methodName, params object[] param)
		{
			var type = typeof(T);
			var methodInfo = type.GetMethod(methodName);
			if (methodInfo == null)
			{
				throw new ArgumentException("invalid methodName.  methodName=" + methodName);
			}

			return methodInfo.Invoke(instance, param);
		}

		/// <summary>
		/// 文字列として渡されたGetプロパティを実行する。
		/// </summary>
		/// <typeparam name="T">実行するプロパティを持つインスタンスの型</typeparam>
		/// <param name="instance">実行するプロパティのインスタンス。静的（static）なメソッドの場合はnull（構造体の場合はnew 構造体名）を指定し、MethodInvoke＜型＞()とする。</param>
		/// <param name="propertyName">プロパティ名</param>
		/// <returns>戻り値。object型であるためキャストすること。</returns>
		internal static object GetPropertyInvoke<T>(T instance, string propertyName)
		{
			var type = typeof(T);
			var propertyInfo = type.GetProperty(propertyName);
			if (propertyInfo == null)
			{
				throw new ArgumentException("invalid instance or propertyName.  propertyName=" + propertyName);
			}

			var methodInfo = propertyInfo.GetGetMethod();	//GetProperty
			if (methodInfo == null)
			{
				throw new ArgumentException("get method can not use.  propertyName=" + propertyName);
			}

			return methodInfo.Invoke(instance, null);
		}

	} //class
}
